package conMgr.data;

import java.io.File;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.w3c.dom.Document;
import org.w3c.dom.Element;

import conMgr.model.Conference;
import conMgr.model.Paper;
import conMgr.model.Review;
import conMgr.model.Status;
import conMgr.model.User;
import conMgr.model.UserType;

/**
 * An XML-based data storage provider.
 * @author is4@uw.edu
 */
public class XmlDataStore extends AbstractXmlDataStore
{
	/**
	 * Initializes a new instance of an XML data store.
	 * @param file The XML file to read and write from.
	 */
	public XmlDataStore(final File file)
	{
		super(file);
	}
	
	/**
	 * Loads all data from the data store.
	 * @return True if all data was loaded successfully; otherwise, false.
	 */
	public boolean loadAll()
	{
		final Document document = getDocument();
		
		if (document == null)
			return false;
		
		final Map<Integer, User> userMap =
			new HashMap<Integer, User>();
		final Map<Integer, Conference> conferenceMap =
			new HashMap<Integer, Conference>();
		final Map<Integer, Paper> paperMap =
			new HashMap<Integer, Paper>();
		
		final Element root = document.getDocumentElement();
		
		return
			loadUsers(root, userMap) &&
			loadConferences(root, userMap, conferenceMap) &&
			loadPapers(root, userMap, conferenceMap, paperMap) &&
			loadReviews(root, userMap, paperMap);
	}
	
	/**
	 * Loads the conferences from the data store.
	 * @param ancestor The XML element to load data from.
	 * @param userMap A map of user IDs (keys) to users (values).
	 * @param conferenceMap A map of conference IDs (keys) to conferences
	 * (values).
	 * @return A flag indicating whether everything was successful.
	 */
	private boolean loadConferences
	(
		final Element ancestor,
		final Map<Integer, User> userMap,
		final Map<Integer, Conference> conferenceMap
	)
	{
		final List<Element> conferences =
			xml.getElements(ancestor, "conferences", "conference");
		if (conferences == null) return false;
		
		for (Element conference : conferences)
		{
			final Integer id = xml.getInt(conference, "id", true);
			if (id == null) return false;
			
			final String name = xml.getString(conference, "name", true);
			if (name == null) return false;
			
			final User programChair =
				xml.getEntity(conference, userMap, "programChair", true);
			if (programChair == null) return false;
			
			final Date submissionDL =
				xml.getDate(conference, "submissionDL", true);
			if (submissionDL == null) return false;

			final Date reviewDL = xml.getDate(conference, "reviewDL", true);
			if (reviewDL == null) return false;

			final Date recommendationDL =
				xml.getDate(conference, "recommendationDL", true);
			if (recommendationDL == null) return false;

			final Date revisionDL =
				xml.getDate(conference, "revisionDL", true);
			if (revisionDL == null) return false;
			
			final Conference conferenceObject =
				new Conference
				(
					name,
					programChair,
					submissionDL,
					reviewDL,
					recommendationDL,
					revisionDL
				);
			
			conferenceMap.put(id, conferenceObject);
			
			if
			(
				!loadSubprogramChairs(conference, conferenceObject, userMap) ||
				!loadReviewers(conference, conferenceObject, userMap)
			)
				return false;
		}
		
		return true;
	}

	/**
	 * Loads the papers from the data store.
	 * @param ancestor The XML element to get data from.
	 * @param userMap A map of user IDs (keys) to users (values).
	 * @param conferenceMap A map of conference IDs (keys) to conferences
	 * (values).
	 * @param paperMap A map of paper IDs (keys) to papers (values).
	 * @return A flag indicating whether everything was successful.
	 */
	private boolean loadPapers
	(
		final Element ancestor,
		final Map<Integer, User> userMap,
		final Map<Integer, Conference> conferenceMap,
		final Map<Integer, Paper> paperMap
	)
	{
		final List<Element> papers =
			xml.getElements(ancestor, "papers", "paper");
		if (papers == null) return false;
		
		for (Element paper : papers)
		{
			final Integer id = xml.getInt(paper, "id", true);
			if (id == null) return false;
			
			final String title = xml.getString(paper, "title", true);
			if (title == null) return false;
			
			final User author = xml.getEntity(paper, userMap, "author", true);
			if (author == null) return false;
			
			final Conference conference =
				xml.getEntity(paper, conferenceMap, "conference", true);
			if (conference == null) return false;
			
			final String paperAbstract = xml.getString(paper, "abstract", true);
			if (paperAbstract == null) return false;
			
			final String docText = xml.getString(paper, "text", true);
			if (docText == null) return false;
			
			final String categories = xml.getString(paper, "categories", true);
			if (categories == null) return false;
			
			final String keywords = xml.getString(paper, "keywords", true);
			if (keywords == null) return false;
			
			final Paper paperObject =
				new Paper
				(
					title,
					author,
					conference,
					paperAbstract,
					categories,
					keywords,
					docText
				);
			
			paperMap.put(id, paperObject);
			
			final User subprogramChair =
				xml.getEntity(paper, userMap, "subprogramChair", false);
			if (subprogramChair == null) continue;

			paperObject.assignSubprogramChair(subprogramChair);
			
			final Integer subprogramChairRating =
				xml.getInt(paper, "subprogramChairRating", false);
			final String subprogramChairRationale =
				xml.getString(paper, "subprogramChairRationale", false);
			if
			(
				subprogramChairRating == null ||
				subprogramChairRationale == null
			)
				continue;
			
			paperObject.submitToProgramChair
			(
				subprogramChairRating,
				subprogramChairRationale
			);
			
			final Status status = xml.getStatus(paper, "status", false);
			
			if (status == Status.ACCEPTED)
				paperObject.accept();
			else if (status == Status.REJECTED)
				paperObject.reject();
		}
		
		return true;
	}

	/**
	 * Loads the conference reviewers from the data store.
	 * @param ancestor The XML element to load data from.
	 * @param conference The conference whose reviewers are being loaded.
	 * @param userMap A map of user IDs (keys) to users (values).
	 * @return A flag indicating whether everything was successful.
	 */
	private boolean loadReviewers
	(
		final Element ancestor,
		final Conference conference,
		final Map<Integer, User> userMap
	)
	{
		final List<Element> reviewers =
			xml.getElements(ancestor, "reviewers", "reviewer");
		if (reviewers == null) return false;
		
		for (Element reviewer : reviewers)
		{
			final User user = xml.getEntity(reviewer, userMap, "user", true);
			if (user == null) return false;

			conference.getReviewers().add(user);
			user.getReivewerConferences().add(conference);
		}
		
		return true;
	}

	/**
	 * Loads the reviews from the data store.
	 * @param ancestor The XML element to get data from.
	 * @param userMap A map of user IDs (keys) to users (values).
	 * @param paperMap A map of paper IDs (keys) to papers (values).
	 * @return A flag indicating whether everything was successful.
	 */
	private boolean loadReviews
	(
		final Element ancestor,
		final Map<Integer, User> userMap,
		final Map<Integer, Paper> paperMap
	)
	{
		final List<Element> reviews =
			xml.getElements(ancestor, "reviews", "review");
		if (reviews == null) return false;
		
		for (Element review : reviews)
		{
			final User reviewer =
				xml.getEntity(review, userMap, "reviewer", true);
			if (reviewer == null) return false;
			
			final Paper paper = xml.getEntity(review, paperMap, "paper", true);
			if (paper == null) return false;
			
			final Review reviewObject = new Review(reviewer, paper);
			
			final String commentsForAuthor =
				xml.getString(review, "commentsForAuthor", false);
			final String commentsForSubprogramChair =
				xml.getString(review, "commentsForSubprogramChair", false);
			final List<Integer> ratings =
				xml.getIntArray(review, "ratings", false);
			final Integer overallRating =
				xml.getInt(review, "overallRating", false);

			if
			(
				commentsForAuthor == null ||
				commentsForSubprogramChair == null ||
				ratings == null ||
				overallRating == null
			)
				continue;

			reviewObject.submitToSubprogramChair
			(
				commentsForAuthor,
				commentsForSubprogramChair,
				ratings,
				overallRating
			);
		}
		
		return true;
	}

	/**
	 * Loads the conference subprogram chairs from the data store.
	 * @param ancestor The XML element to load data from.
	 * @param conference The conference whose subprogram chairs are being
	 * loaded.
	 * @param userMap A map of user IDs (keys) to users (values).
	 * @return A flag indicating whether everything was successful.
	 */
	private boolean loadSubprogramChairs
	(
		final Element ancestor,
		final Conference conference,
		final Map<Integer, User> userMap
	)
	{
		final List<Element> subprogramChairs =
			xml.getElements(ancestor, "subprogramChairs", "subprogramChair");
		if (subprogramChairs == null) return false;
		
		for (Element subprogramChair : subprogramChairs)
		{
			final User user =
				xml.getEntity(subprogramChair, userMap, "user", true);
			if (user == null) return false;

			conference.getSubprogramChairs().add(user);
			user.getSubprogramChairConferences().add(conference);
		}
		
		return true;
	}

	/**
	 * Loads the users from the data store.
	 * @param ancestor The XML element to get data from.
	 * @param userMap A map of user IDs (keys) to users (values).
	 * @return A flag indicating whether everything was successful.
	 */
	private boolean loadUsers
	(
		final Element ancestor,
		final Map<Integer, User> userMap
	)
	{
		final List<Element> users =
			xml.getElements(ancestor, "users", "user");
		
		if (users == null)
			return false;
		
		for (Element user : users)
		{
			final Integer id = xml.getInt(user, "id", true);
			if (id == null) return false;
			
			final String name = xml.getString(user, "name", true);
			if (name == null) return false;
			
			final String email = xml.getString(user, "email", true);
			if (email == null) return false;

			final UserType type = xml.getType(user, "type", true);
			if (type == null) return false;
			
			userMap.put(id, new User(name, email, type));
		}
		
		return true;
	}
	
	/**
	 * Saves all data to the data store.
	 * @return True if all data was saved successfully; otherwise, false.
	 */
	public boolean saveAll()
	{
		final Document document = newDocument();
		
		if (document == null)
			return false;
		
		final Map<User, Integer> userMap =
			new HashMap<User, Integer>();
		final Map<Conference, Integer> conferenceMap =
			new HashMap<Conference, Integer>();
		final Map<Paper, Integer> paperMap =
			new HashMap<Paper, Integer>();
		
		final Element root = document.createElement("data");
		document.appendChild(root);
		
		return
			saveUsers(root, userMap) &&
			saveConferences(root, userMap, conferenceMap) &&
			savePapers(root, userMap, conferenceMap, paperMap) &&
			saveReviews(root, userMap, paperMap) &&
			persist(document);
	}

	/**
	 * Saves the conferences to the data store.
	 * @param root The root element to append the conferences to.
	 * @param userMap A map of users (keys) to user IDs (values).
	 * @param conferenceMap A map of conferences (keys) to conference IDs
	 * (values).
	 * @return A flag indicating whether everything was successful.
	 */
	private boolean saveConferences
	(
		final Element root,
		final Map<User, Integer> userMap,
		final Map<Conference, Integer> conferenceMap
	)
	{
		final Element conferences = xml.appendNode(root, "conferences");
		Integer id = 0;
		
		for (Conference conferenceObject : Conference.getAllConferences())
		{
			final Element conference =
				xml.appendNode(conferences, "conference");
			
			conferenceMap.put(conferenceObject, ++id);
			xml.appendValue(conference, "id", id);
			xml.appendValue(conference, "name", conferenceObject.getName());
			
			final User user = conferenceObject.getProgramChair(); 
			if (!xml.appendValue(conference, "programChair", user, userMap))
				return false;
			
			Date date = conferenceObject.getSubmissionDL(); 
			xml.appendValue(conference, "submissionDL", date);
			
			date = conferenceObject.getReviewDL();
			xml.appendValue(conference, "reviewDL", date);
			
			date = conferenceObject.getRecommendationDL();
			xml.appendValue(conference, "recommendationDL", date);
			
			date = conferenceObject.getRevisionDL();
			xml.appendValue(conference, "revisionDL", date);
			
			if
			(
				!saveSubprogramChairs(conference, conferenceObject, userMap) ||
				!saveReviewers(conference, conferenceObject, userMap)
			)
				return false;
		}
		
		return true;
	}

	/**
	 * Saves the papers to the data store.
	 * @param root The root element to append paper elements to.
	 * @param userMap A map of users (keys) to user IDs (values).
	 * @param conferenceMap A map of conferences (keys) to conference IDs
	 * (values).
	 * @param paperMap A map of papers (keys) to paper IDs (values).
	 * @return A flag indicating whether everything was successful.
	 */
	private boolean savePapers
	(
		final Element root,
		final Map<User, Integer> userMap,
		final Map<Conference, Integer> conferenceMap,
		final Map<Paper, Integer> paperMap
	)
	{
		final List<Paper> allPapers = new LinkedList<Paper>();
		
		for (Conference conference : Conference.getAllConferences())
			allPapers.addAll(conference.getPapers());
		
		final Element papers = xml.appendNode(root, "papers");
		Integer id = 0;
		
		for (Paper paperObject : allPapers)
		{
			final Element paper = xml.appendNode(papers, "paper");
			
			paperMap.put(paperObject, ++id);
			xml.appendValue(paper, "id", id);
			xml.appendValue(paper, "title", paperObject.getTitle());
			
			User user = paperObject.getAuthor(); 
			if (!xml.appendValue(paper, "author", user, userMap))
				return false;
			
			final Conference conf = paperObject.getConference();
			if (!xml.appendValue(paper, "conference", conf, conferenceMap))
				return false;
			
			xml.appendValue(paper, "abstract", paperObject.getPaperAbstract());
			xml.appendValue(paper, "keywords", paperObject.getKeywords());
			xml.appendValue(paper, "categories", paperObject.getCategories());
			xml.appendValue(paper, "text", paperObject.getDocText());
			
			user = paperObject.getSubprogramChair();
			if (user == null)
				continue;
			if (!xml.appendValue(paper, "subprogramChair", user, userMap))
				return false;
			
			final Integer rating = paperObject.getSubprogramChairRating();
			final String rationale = paperObject.getSubprogramChairRationale();
			if (rating == null || rationale == null)
				continue;
			xml.appendValue(paper, "subprogramChairRating", rating);
			xml.appendValue(paper, "subprogramChairRationale", rationale);
			
			final Status status = paperObject.getStatus();
			if (status == Status.UNDECIDED || status == Status.RECOMMENDED)
				continue;
			
			xml.appendValue(paper, "status", status);
		}
		
		return true;
	}

	/**
	 * Saves the conference reviewers to the data store.
	 * @param ancestor The XML element to save data to.
	 * @param conference The conference whose reviewers are being saved. 
	 * @param userMap A map of users (keys) to user IDs (values).
	 * @return A flag indicating whether everything was successful.
	 */
	private boolean saveReviewers
	(
		final Element ancestor,
		final Conference conference,
		final Map<User, Integer> userMap
	)
	{
		final Element users = xml.appendNode(ancestor, "reviewers");
		
		for (User user : conference.getReviewers())
		{
			final Element reviewer =
				xml.appendNode(users, "reviewer");
			
			if (!xml.appendValue(reviewer, "user", user, userMap))
				return false;
		}
		
		return true;
	}

	/**
	 * Saves the reviews to the data store.
	 * @param root The root element to append review elements to.
	 * @param userMap A map of users (keys) to user IDs (values).
	 * @param paperMap A map of papers (keys) to paper IDs (values).
	 * @return A flag indicating whether everything was successful.
	 */
	private boolean saveReviews
	(
		final Element root,
		final Map<User, Integer> userMap,
		final Map<Paper, Integer> paperMap
	)
	{
		final List<Review> allReviews = new LinkedList<Review>();
		
		for (Conference conference : Conference.getAllConferences())
			for (Paper paper : conference.getPapers())
				allReviews.addAll(paper.getPaperReviews());
		
		final Element reviews = xml.appendNode(root, "reviews");
		
		for (Review reviewObject : allReviews)
		{
			final Element review = xml.appendNode(reviews, "review");
			
			final User user = reviewObject.getReviewer();
			if (!xml.appendValue(review, "reviewer", user, userMap))
				return false;
			
			final Paper paper = reviewObject.getPaper();
			if (!xml.appendValue(review, "paper", paper, paperMap))
				return false;
			
			final String authorComments = reviewObject.getcommentsForAuthor();
			final String spcComments = reviewObject.getcommentsForSpChair();
			final List<Integer> ratings = reviewObject.getRating();
			final Integer overallRating = reviewObject.getOverallRatings();
			
			if
			(
				authorComments == null ||
				spcComments == null ||
				ratings == null ||
				overallRating == null
			)
				continue;
			
			xml.appendValue(review, "commentsForAuthor", authorComments);
			xml.appendValue(review, "commentsForSubprogramChair", spcComments);
			xml.appendValue(review, "ratings", ratings);
			xml.appendValue(review, "overallRating", overallRating);
		}
		
		return true;
	}

	/**
	 * Saves the conference subprogram chairs to the data store.
	 * @param ancestor The XML element to save data to.
	 * @param conference The conference whose subprogram chairs are being saved. 
	 * @param userMap A map of users (keys) to user IDs (values).
	 * @return A flag indicating whether everything was successful.
	 */
	private boolean saveSubprogramChairs
	(
		final Element ancestor,
		final Conference conference,
		final Map<User, Integer> userMap
	)
	{
		final Element users = xml.appendNode(ancestor, "subprogramChairs");
		
		for (User user : conference.getSubprogramChairs())
		{
			final Element subprogramChair =
				xml.appendNode(users, "subprogramChair");
			
			if (!xml.appendValue(subprogramChair, "user", user, userMap))
				return false;
		}
		
		return true;
	}

	/**
	 * Saves the users to the data store.
	 * @param root The root element to append user elements to.
	 * @param userMap A map of users (keys) to user IDs (values).
	 * @return A flag indicating whether everything was successful.
	 */
	private boolean saveUsers
	(
		final Element root,
		final Map<User, Integer> userMap
	)
	{
		final Element users = xml.appendNode(root, "users");
		Integer id = 0;
		
		for (User userObject : User.getAllUsers())
		{
			final Element user = xml.appendNode(users, "user");
			
			userMap.put(userObject, ++id);
			xml.appendValue(user, "id", id);
			xml.appendValue(user, "name", userObject.getName());
			xml.appendValue(user, "email", userObject.getEmail());
			xml.appendValue(user, "type", userObject.getType());
		}
		
		return true;
	}
}
